package com.gdevelop.gwtxp.rebind.el;

import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JMethod;
import com.google.gwt.core.ext.typeinfo.JParameter;
import com.google.gwt.core.ext.typeinfo.JPrimitiveType;
import com.google.gwt.core.ext.typeinfo.JType;

import com.google.gwt.core.ext.typeinfo.NotFoundException;
import com.google.gwt.core.ext.typeinfo.TypeOracle;

import java.util.HashMap;
import java.util.Map;


public class TypeIntrospector {
  public static class PropertyDescriptor{
    private String name;
    private JClassType type;
    private JMethod readMethod;
    private JMethod writeMethod;
    
    public PropertyDescriptor(String name, JClassType type, JMethod readMethod, JMethod writeMethod){
      this.name = name;
      this.type = type;
      this.readMethod = readMethod;
      this.writeMethod = writeMethod;
    }
    
    public String getName(){
      return name;
    }
    public JClassType getType(){
      return type;
    }
    public JMethod getReadMethod(){
      return readMethod;
    }
    public JMethod getWriteMethod(){
      return writeMethod;
    }
  }
  
  private GeneratorContext context;
  public TypeIntrospector(GeneratorContext context){
    this.context = context;
  }
  /**
   * Auto-boxing premilitary types to Object type
   */
  public JClassType autoboxed(JType type) {
    TypeOracle typeOracle = context.getTypeOracle();
    JPrimitiveType primitiveType = type.isPrimitive();
    if (type.isPrimitive() != null){
      try{
        if (primitiveType == JPrimitiveType.FLOAT)
          return typeOracle.getType(Float.class.getName());
        if (primitiveType == JPrimitiveType.DOUBLE)
          return typeOracle.getType(Double.class.getName());
        if (primitiveType == JPrimitiveType.LONG)
          return typeOracle.getType(Long.class.getName());
        if (primitiveType == JPrimitiveType.INT)
          return typeOracle.getType(Integer.class.getName());
        if (primitiveType == JPrimitiveType.SHORT)
          return typeOracle.getType(Short.class.getName());
        if (primitiveType == JPrimitiveType.BYTE)
          return typeOracle.getType(Byte.class.getName());
        if (primitiveType == JPrimitiveType.CHAR)
          return typeOracle.getType(Character.class.getName());
        if (primitiveType == JPrimitiveType.BOOLEAN)
          return typeOracle.getType(Boolean.class.getName());
      }catch(NotFoundException e){
        // No-op, should not happen
      }
    }
    return (JClassType)type;
  }


  public PropertyDescriptor getPropertyDescriptor(JClassType classType, String propName){
    PropertyDescriptor[] propDescriptors = getPropertyDescriptors(classType);
    for (PropertyDescriptor propDescriptor : propDescriptors){
      if (propDescriptor.getName().equals(propName)){
        return propDescriptor;
      }
    }
    
    return null;
  }
  
  public PropertyDescriptor[] getPropertyDescriptors(JClassType classType){
    Map<String, PropertyDescriptor> props = new HashMap<String, PropertyDescriptor>();
    
    JMethod[] methods = getMethods(classType);
    for (JMethod method : methods){
      String methodName = method.getName();
      JClassType propType = null;
      JMethod readMethod = null;
      JMethod writeMethod = null;

      if (methodName.startsWith("get")){
        methodName = methodName.substring(3);
        if (JPrimitiveType.VOID.equals(method.getReturnType())){
          continue;
        }
        if (method.getParameters().length != 0){
          continue;
        }
        if (!method.isPublic()){
          continue;
        }
        
        propType = autoboxed(method.getReturnType());
        readMethod = method;
      }else if (methodName.startsWith("is")){
        methodName = methodName.substring(2);
        if (JPrimitiveType.VOID.equals(method.getReturnType())){
          continue;
        }
        if (method.getParameters().length != 0){
          continue;
        }
        if (!method.isPublic()){
          continue;
        }

        propType = autoboxed(method.getReturnType());
        readMethod = method;
      }else if (methodName.startsWith("set")){
        if (!JPrimitiveType.VOID.equals(method.getReturnType())){
          continue;
        }
        if (method.getParameters().length != 1){
          continue;
        }
        if (!method.isPublic()){
          continue;
        }

        methodName = methodName.substring(3);
        propType = autoboxed(method.getParameters()[0].getType());
        writeMethod = method;
      }else{
        continue;
      }
      if (methodName.length() <= 0){
        continue;
      }
      String propName = methodName.substring(0, 1).toLowerCase() + methodName.substring(1);
      if (isAllUpperCase(methodName)){
        propName = methodName;
      }
      PropertyDescriptor propDescriptor;
      if ((propDescriptor = props.get(propName)) == null){
        props.put(propName, new PropertyDescriptor(propName, propType, readMethod, writeMethod));
      }else{
        if (readMethod != null){
          propDescriptor.readMethod = readMethod;
        }
        if (writeMethod != null){
          propDescriptor.writeMethod = writeMethod;
        }
      }
    }
    
    return props.values().toArray(new PropertyDescriptor[props.size()]);
  }

  public JMethod[] getMethods(JClassType type) {
    Map<String, JMethod> methods = new HashMap<String, JMethod>();
    getMethodsInternal(type, methods);
    
    return methods.values().toArray(new JMethod[methods.size()]);
  }
  
  private void getMethodsInternal(JClassType type, Map<String, JMethod> m) {
    for (JMethod method : type.getMethods()){
      /*
      String methodName = method.getName();
      if (methodName.startsWith("get")){
        methodName = methodName.substring(3);
      }else if (methodName.startsWith("is")){
        methodName = methodName.substring(2);
      }else if (methodName.startsWith("set")){
        methodName = methodName.substring(3);
      }else{
        continue;
      }
      */

      String s = uniqueMethodKey(method);
      if (m.get(s) == null) {
        m.put(s, method);
      }
    }
    
    if (type.getSuperclass() != null){
      getMethodsInternal(type.getSuperclass(), m);
    }
    
    for (JClassType superInterface : type.getImplementedInterfaces()){
      getMethodsInternal(superInterface, m);
    }
  }

  private static String uniqueMethodKey(JMethod method) {
    String name = method.getName();
    name += "(";
    JParameter[] m = method.getParameters();
    for (int i = 0; i < m.length; i++) {
      name += m[i].getType() + " ";
    }
    name += ")";
    return name;
  }
  
  private boolean isAllUpperCase(String s){
    for (int i=0; i<s.length(); i++){
      if ((s.charAt(i) < 'A') || (s.charAt(i) > 'Z')){
        return false;
      }
    }
    return true;
  }
}
